package com.myrslok.module;

import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xml.sax.*;
import org.xml.sax.helpers.DefaultHandler;

import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import java.io.*;
import java.util.HashSet;
import java.util.Set;

/**
 * Module settings that are backed by the Idea module and project files.
 */
public class IdeaModuleSettings implements ModuleSettings {

    private static final Logger logger = LoggerFactory.getLogger(IdeaModuleSettings.class);

    private static final FileFilter IML_FILE_FILTER = new FileFilter() {
        @Override
        public boolean accept(@NotNull final File pathname) {
            return pathname.getName().endsWith(".iml");
        }
    };
    private static final String IDEA_HOME_ENV_VARIABLE = "IDEA_HOME";
    private static final String IDEA_HOME = System.getenv(IDEA_HOME_ENV_VARIABLE);
    private String moduleName;
    private final File moduleRoot;
    private ImlSetup imlSetup;

    public IdeaModuleSettings(String moduleName) {
        this.moduleName = moduleName;
        this.moduleRoot = new File(moduleName);
        File[] imlFiles = this.moduleRoot.listFiles(IML_FILE_FILTER);
        if (imlFiles.length != 1) {
            throw new RuntimeException(this.getClass().getSimpleName() + " currently only handles 1 iml file (found " + imlFiles.length + ")");
        }
        imlSetup = parseImlFile(imlFiles[0]);
    }


    @Override
    @NotNull
    public File getModuleRoot() {
        return moduleRoot;
    }

    @Override
    @NotNull
    public Set<File> getSrcDirs() {
        return imlSetup.getProdDirs();
    }

    @Override
    @NotNull
    public Set<File> getTestDirs() {
        return imlSetup.getTestDirs();
    }

    @Override
    @NotNull
    public File getBuildDir() {
        // todo look for MyrslokBuild in moduleRoot?
        return null;
    }

    @Override
    @NotNull
    public File getTargetDir() {
        return moduleRoot;
    }

    @Override
    @NotNull
    public File getTargetProdClassesDir() {
        if (imlSetup.isInheritCompilerOutput()) {
            ProjectSetup projectSetup = parseProjectFile();
            return projectSetup.getTargetProduction();
        } else {
            return imlSetup.getTargetProduction();
        }
    }

    @Override
    @NotNull
    public File getTargetTestClassesDir() {
        if (imlSetup.isInheritCompilerOutput()) {
            ProjectSetup projectSetup = parseProjectFile();
            return projectSetup.getTargetProduction();
        } else {
            return imlSetup.getTargetProduction();
        }
    }

    @Override
    @NotNull
    public File getTargetBuildClassesDir() {
        // use test for Myrslok build scripts
        return getTargetTestClassesDir();
    }


    @Override
    @NotNull
    public Set<File> getProductionLibs() {
        return imlSetup.getProdLibs();
    }

    @Override
    @NotNull
    public Set<File> getTestLibs() {
        return imlSetup.getTestLibs();
    }


    private ImlSetup parseImlFile(@NotNull final File file) {
        final ImlSetup imlSetup = new ImlSetup();
        parseFile(file, imlSetup);
        return imlSetup;
    }

    private ProjectSetup parseProjectFile() {
        final ProjectSetup projectSetup = new ProjectSetup();
        parseFile(new File(".idea/misc.xml"), projectSetup);
        return projectSetup;
    }

    private void parseFile(File file, ContentHandler contentHandler) {
        InputStream inputStream = null;
        try {
            inputStream = new BufferedInputStream(new FileInputStream(file));
            parseXml(contentHandler, inputStream);
        }
        catch (FileNotFoundException e) {
            logger.error("File " + file + " not found", e);
        }
        finally {
            if (inputStream != null) {
                try {
                    inputStream.close();
                } catch (IOException e) {
                    logger.error("XML Passing Exception (close)", e);
                }
            }
        }
    }

    private void parseXml(final ContentHandler contentHandler, final InputStream inputStream) {
        final SAXParserFactory spf = SAXParserFactory.newInstance();
        try {
            final SAXParser sp = spf.newSAXParser();
            final XMLReader xr = sp.getXMLReader();

            xr.setContentHandler(contentHandler);
            xr.parse(new InputSource(inputStream));

        } catch (ParserConfigurationException e) {
            logger.error("Parse " + contentHandler.getClass().getSimpleName() + " File", "XML Passing Exception = ", e);
        } catch (IOException e) {
            logger.error("Parse " + contentHandler.getClass().getSimpleName() + " File", "XML Passing Exception = ", e);
        } catch (SAXException e) {
            logger.error("Parse " + contentHandler.getClass().getSimpleName() + " File", "XML Passing Exception = ", e);
        }
    }

    /**
     * <xxx url="file://$MODULE_DIR$/src/main/java"/>  ->  File: {moduleRoot}/src/main/java
     * @param attributes to get url from
     * @param root
     * @param str
     * @return a File with the path found in the url
     */
    private File getUrlAfterString(@NotNull final Attributes attributes, final File root, final String str) {
        String url = attributes.getValue("url");
        if (url == null) {
            throw new RuntimeException("No 'url' attribute.");
        }
        int index = url.indexOf(str);
        if (index == -1) {
            throw new RuntimeException("'"+ str + "' not present in url attribute value (" + url + ").");
        }
        return new File(root, url.substring(index + str.length()));
    }

    private class ImlSetup extends DefaultHandler {
        private static final String JAR_PREFIX = "jar://";
        @NotNull
        private final Set<File> prodDirs = new HashSet<File>();
        @NotNull
        private final Set<File> testDirs = new HashSet<File>();
        private Boolean inheritCompilerOutput;
        private File targetProduction;
        private File targetTest;

        private String orderEntryType;
        private String orderEntryScope;
        private Set<File> prodLibs = new HashSet<File>();
        private Set<File> testLibs = new HashSet<File>();

        @NotNull
        public Set<File> getProdDirs() {
            return prodDirs;
        }

        @NotNull
        public Set<File> getTestDirs() {
            return testDirs;
        }

        public boolean isInheritCompilerOutput() {
            if (inheritCompilerOutput == null) {
                throw new RuntimeException("Module iml file does not have inherit-compiler-output defined.");
            }
            return inheritCompilerOutput.booleanValue();
        }

        public File getTargetProduction() {
            return targetProduction;
        }

        public File getTargetTest() {
            return targetTest;
        }

        public Set<File> getProdLibs() {
            return prodLibs;
        }

        public Set<File> getTestLibs() {
            return testLibs;
        }

        @Override
        public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
            parseSourceFolder(qName, attributes);
            parseComponent(qName, attributes);
            parseOutput(qName, attributes);
            parseOutputTest(qName, attributes);

            parseOrderEntry(qName, attributes);
        }

        @Override
        public void endElement(String uri, String localName, String qName) throws SAXException {

        }

        private void parseOrderEntry(String qName, Attributes attributes) {
            //    <orderEntry type="module-library" scope="TEST">
            //      <library name="Java tools">
            //        <CLASSES>
            //          <root url="jar://C:/jdk/jdk1.7.0/lib/tools.jar!/" />
            //        </CLASSES>
            //        <JAVADOC />
            //        <SOURCES />
            //      </library>
            //    </orderEntry>
            if (qName.equals("orderEntry")) {
                String type = attributes.getValue("type");
                if (type != null) {
                    orderEntryType = type;
                }
                String scope = attributes.getValue("scope");
                if (scope == null) {
                    orderEntryScope = null;
                } else {
                    orderEntryScope = scope;
                }
            }
            if (qName.equals("root")) {
                String url = attributes.getValue("url");
                assert "module-library".equals(orderEntryType);
                if (url.startsWith(JAR_PREFIX)) {
                    File jar = getJarLib(url);
                    if (orderEntryScope == null) {
                        prodLibs.add(jar);
                    } else if (orderEntryScope.equals("TEST")) {
                        testLibs.add(jar);
                    }
                }
            }
        }

        private File getJarLib(String url) {
            String location = url.substring(JAR_PREFIX.length(), url.length() - 2);
            if (location.startsWith("$MODULE_DIR$/")) {
                return new File(moduleRoot, location.substring("$MODULE_DIR$/".length()));
            }
            if (location.startsWith("$APPLICATION_HOME_DIR$/")) {
                if (IDEA_HOME != null) {
                    return new File(new File(IDEA_HOME), location.substring("$APPLICATION_HOME_DIR$/".length()));
                } else {
                    logger.error("Using IDEA libs and environment variable " + IDEA_HOME_ENV_VARIABLE + " not set.");
                }
            }
            return new File(location);
        }

        private void parseOutput(String qName, Attributes attributes) {
            //   <output url="file://$MODULE_DIR$/target/idea-classes/production" />
            if (qName.equals("output")) {
                targetProduction = getUrlAfterString(attributes, IdeaModuleSettings.this.moduleRoot, "file://$MODULE_DIR$/");
            }
        }

        private void parseOutputTest(String qName, Attributes attributes) {
            //    <output-test url="file://$MODULE_DIR$/target/idea-classes/test" />
            if (qName.equals("output-test")) {
                targetProduction = getUrlAfterString(attributes, IdeaModuleSettings.this.moduleRoot, "file://$MODULE_DIR$/");
            }
        }

        private void parseComponent(String qName, Attributes attributes) {
            //   <component name="NewModuleRootManager" inherit-compiler-output="true">
            if (qName.equals("component")) {
                inheritCompilerOutput = Boolean.valueOf(attributes.getValue("inherit-compiler-output"));
            }
        }

        private void parseSourceFolder(String qName, Attributes attributes) {
            //   <sourceFolder url="file://$MODULE_DIR$/src/main/java" isTestSource="false" />
            if (qName.equals("sourceFolder")) {
                File srcDir = getUrlAfterString(attributes, IdeaModuleSettings.this.moduleRoot, "file://$MODULE_DIR$/");
                if (Boolean.valueOf(attributes.getValue("isTestSource")).booleanValue()) {
                    testDirs.add(srcDir);
                } else {
                    prodDirs.add(srcDir);
                }
            }
        }

    }

    private class ProjectSetup extends DefaultHandler {
        private File targetProduction;
        private File targetTest;


        public File getTargetProduction() {
            return targetProduction;
        }

        public File getTargetTest() {
            return targetTest;
        }


        @Override
        public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
            parseOutput(qName, attributes);
        }

        private void parseOutput(String qName, Attributes attributes) {
            //    <output url="file://$PROJECT_DIR$/target/idea-classes" />
            if (qName.equals("output")) {
                File baseTargetPath = getUrlAfterString(attributes, new File("."), "file://$PROJECT_DIR$/");
                targetProduction = new File(baseTargetPath, "Production/" + moduleName);
                targetTest = new File(baseTargetPath, "Test/" + moduleName);
            }
        }
    }
}
